Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 3bbee7eed0ff214306d3aec017331cdb4f73d175


Parents : 4a34921
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-22T15:05:40-05:00

feat(database): update trim_announces_for_aspect to protect favourited destinations and saved contacts from deletion

Changes

2 files changed, 142 insertions(+), 27 deletions(-)


Diff

diff --git a/meshchatx/src/backend/database/announces.py b/meshchatx/src/backend/database/announces.py
index 16f545aa..125f59d8 100644
--- a/meshchatx/src/backend/database/announces.py
+++ b/meshchatx/src/backend/database/announces.py
@@ -50,7 +50,15 @@ class AnnounceDAO:
self.provider.execute(query, params)
def trim_announces_for_aspect(self, aspect, max_rows):
- """Delete oldest rows for this aspect until at most max_rows remain."""
+ """Delete oldest rows for this aspect until at most max_rows remain.
+
+ Announces that correspond to a favourited destination or to a saved
+ contact are considered protected and are never deleted by this trim,
+ even if the total count exceeds ``max_rows``. This prevents purging
+ of announces (and the path/identity context they provide) for
+ favourited NomadNet nodes and for messaging contacts when storage
+ limits are enforced.
+ """
if max_rows < 1 or not aspect:
return
row = self.provider.fetchone(
@@ -64,8 +72,19 @@ class AnnounceDAO:
self.provider.execute(
"""
DELETE FROM announces WHERE id IN (
- SELECT id FROM announces WHERE aspect = ?
- ORDER BY updated_at ASC, id ASC
+ SELECT a.id FROM announces a
+ WHERE a.aspect = ?
+ AND NOT EXISTS (
+ SELECT 1 FROM favourite_destinations f
+ WHERE f.destination_hash = a.destination_hash
+ )
+ AND NOT EXISTS (
+ SELECT 1 FROM contacts c
+ WHERE c.remote_identity_hash = a.identity_hash
+ OR c.lxmf_address = a.destination_hash
+ OR c.lxst_address = a.destination_hash
+ )
+ ORDER BY a.updated_at ASC, a.id ASC
LIMIT ?
)
""",

diff --git a/tests/backend/test_announce_dao_trim.py b/tests/backend/test_announce_dao_trim.py
index 36313ea5..d78c6b23 100644
--- a/tests/backend/test_announce_dao_trim.py
+++ b/tests/backend/test_announce_dao_trim.py
@@ -7,12 +7,12 @@ from meshchatx.src.backend.database import Database
from meshchatx.src.backend.database.provider import DatabaseProvider
-def _insert(db, dest_hex, aspect, updated_order):
+def _insert(db, dest_hex, aspect, updated_order, identity_hex=None):
db.announces.upsert_announce(
{
"destination_hash": dest_hex,
"aspect": aspect,
- "identity_hash": "a" * 32,
+ "identity_hash": identity_hex or ("a" * 32),
"identity_public_key": "cHVibmtleQ==",
"app_data": None,
"rssi": None,
@@ -26,14 +26,37 @@ def _insert(db, dest_hex, aspect, updated_order):
)
+def _new_db():
+ with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
+ path = f.name
+ db = Database(path)
+ db.initialize()
+ return db, path
+
+
+def _cleanup(db, path):
+ if db is not None:
+ try:
+ db.close()
+ except Exception:
+ pass
+ DatabaseProvider._instance = None
+ if path:
+ try:
+ os.unlink(path)
+ except OSError:
+ pass
+ for suffix in ("-wal", "-shm"):
+ try:
+ os.unlink(path + suffix)
+ except OSError:
+ pass
+
+
def test_trim_announces_for_aspect_drops_oldest():
- path = None
- db = None
+ db = path = None
try:
- with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
- path = f.name
- db = Database(path)
- db.initialize()
+ db, path = _new_db()
aspect = "lxmf.delivery"
_insert(db, "01" * 16, aspect, "2000-01-01T00:00:00Z")
_insert(db, "02" * 16, aspect, "2000-01-02T00:00:00Z")
@@ -43,19 +66,92 @@ def test_trim_announces_for_aspect_drops_oldest():
hashes = {r["destination_hash"] for r in rows}
assert hashes == {"03" * 16, "02" * 16}
finally:
- if db is not None:
- try:
- db.close()
- except Exception:
- pass
- DatabaseProvider._instance = None
- if path:
- try:
- os.unlink(path)
- except OSError:
- pass
- for suffix in ("-wal", "-shm"):
- try:
- os.unlink(path + suffix)
- except OSError:
- pass
+ _cleanup(db, path)
+
+
+def test_trim_preserves_favourited_destination():
+ """Favourited NomadNet/destination announces must survive aspect trim."""
+ db = path = None
+ try:
+ db, path = _new_db()
+ aspect = "nomadnetwork.node"
+
+ _insert(db, "01" * 16, aspect, "2000-01-01T00:00:00Z")
+ _insert(db, "02" * 16, aspect, "2000-01-02T00:00:00Z")
+ _insert(db, "03" * 16, aspect, "2000-01-03T00:00:00Z")
+ _insert(db, "04" * 16, aspect, "2000-01-04T00:00:00Z")
+
+ db.announces.upsert_favourite("01" * 16, "Favourite Node", aspect)
+
+ db.announces.trim_announces_for_aspect(aspect, 2)
+
+ rows = db.announces.get_announces(aspect=aspect)
+ hashes = {r["destination_hash"] for r in rows}
+
+ assert "01" * 16 in hashes, "favourited announce was wrongly trimmed"
+ assert "04" * 16 in hashes, "newest announce should be retained"
+ assert "02" * 16 not in hashes
+ assert "03" * 16 not in hashes
+ finally:
+ _cleanup(db, path)
+
+
+def test_trim_preserves_contact_announce_by_identity_hash():
+ """Announces tied to a saved contact via identity hash must not be dropped."""
+ db = path = None
+ try:
+ db, path = _new_db()
+ aspect = "lxmf.delivery"
+
+ contact_identity = "b" * 32
+
+ _insert(db, "01" * 16, aspect, "2000-01-01T00:00:00Z", contact_identity)
+ _insert(db, "02" * 16, aspect, "2000-01-02T00:00:00Z")
+ _insert(db, "03" * 16, aspect, "2000-01-03T00:00:00Z")
+ _insert(db, "04" * 16, aspect, "2000-01-04T00:00:00Z")
+
+ db.contacts.add_contact(
+ name="Friend",
+ remote_identity_hash=contact_identity,
+ )
+
+ db.announces.trim_announces_for_aspect(aspect, 2)
+
+ rows = db.announces.get_announces(aspect=aspect)
+ hashes = {r["destination_hash"] for r in rows}
+
+ assert "01" * 16 in hashes, "contact-linked announce was wrongly trimmed"
+ assert "04" * 16 in hashes
+ finally:
+ _cleanup(db, path)
+
+
+def test_trim_preserves_contact_announce_by_lxmf_address():
+ """Announces matching contacts.lxmf_address must be retained."""
+ db = path = None
+ try:
+ db, path = _new_db()
+ aspect = "lxmf.delivery"
+
+ protected_dest = "01" * 16
+
+ _insert(db, protected_dest, aspect, "2000-01-01T00:00:00Z")
+ _insert(db, "02" * 16, aspect, "2000-01-02T00:00:00Z")
+ _insert(db, "03" * 16, aspect, "2000-01-03T00:00:00Z")
+ _insert(db, "04" * 16, aspect, "2000-01-04T00:00:00Z")
+
+ db.contacts.add_contact(
+ name="Friend",
+ remote_identity_hash="c" * 32,
+ lxmf_address=protected_dest,
+ )
+
+ db.announces.trim_announces_for_aspect(aspect, 2)
+
+ rows = db.announces.get_announces(aspect=aspect)
+ hashes = {r["destination_hash"] for r in rows}
+
+ assert protected_dest in hashes
+ assert "04" * 16 in hashes
+ finally:
+ _cleanup(db, path)


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────